#!/usr/bin/python
######
#       filemodule.py
#       Copyright Etelos Inc. 2007
#       Darien Hager
#       darien@etelos.com
######

import threading, thread, time, os, sets
from threading import Thread
import hotshot


class folder:
	def __init__(self,name):
		self.__parent = None
		self.__name = name
		self.__data = {}

	def __str__(self):
		return("D "+self.__name)		
	def getName(self):
		return self.__name
	def isFolder(self):
		return True
		
	def add(self,item):
		if item in self.__data:
			return False
		self.__data[item.getName()] = item
		item.setParent(self)
		return True

	def getContents(self):
		arr = set()
		for key in self.__data:
			tup = ( self.__data[key].isFolder() , key )
			arr.add(tup)			
		return arr
	def removeItem(self,item):
		name = item.getName()
		if name in self.__data:
			item = self.__data[name]
			item.setParent(None)
			del self.__data[name]
			return item
		return None
	def removeName(self,name):
		
		if name in self.__data:
			item = self.__data[name]
			item.setParent(None)
			del self.__data[name]
			return item
		return None
			
	def get(self, name):		
		if name in self.__data:
			return self.__data[name]				
		else:
			return None	
	
	def setParent(self,par):
		self.__parent = par
	def getParent(self):
		return self.__parent
	
	def getPathbits(self):
		if(self.__parent == None):
			if(self.__name != ''):
				return [self.__name]
			else:
				return []
		else:
			prev = self.__parent.getPathbits()
			prev.append(self.__name)
			return prev
		
	def getDescendantFolder(self,pathbits, index = 0, create = False):
		if((len(pathbits) <= index) or (pathbits[index] == self.__name)):
			return self
		else:
			targ = pathbits[index]
			res = self.get(targ)
			if(res != None):
				return res.getDescendantFolder(pathbits,index+1,create)
			elif(create):
				new = folder(targ)
				self.add(new)
				return new.getDescendantFolder(pathbits,index+1,create)
			else:
				#assert(False), "Could not get proper folder when given :"+str(pathbits)
				return None
				
	def getAllFiles(self):		
		ret = []
		for keyname in self.__data:
			child = self.__data[keyname]
			if(child.isFolder()):
				ret.extend(child.getAllFiles())
			else:
				ret.append(child)
		return ret
		

class file:		
	def __init__(self,name,modtime = ""):
		self.__parent = None
		self.__name = name
		self.modtime = modtime
	def __str__(self):
		return("F "+self.__name)		
	def getParent(self):
		return self.__parent
	def setParent(self,parent):
		self.__parent = parent
	def isFolder(self):
		return False
	def getName(self):
		return self.__name
	def getPathbits(self):
		if(self.__parent == None):
			if(self.__name != ''):
				return [self.__name]
			else:
				return []
		else:
			prev = self.__parent.getPathbits()
			prev.append(self.__name)
			return prev
	

class filemodule(Thread):
	"""The filemonitor is responsible for periodically checking for changes to 
	files within a specified directory and reporting that information back to
	a specified observer."""
	
	
	
	def __init__(self, root, items = folder("")):
		"""Required values are the directory to watch (an absolute path),
		the observing object, and a textual name of an attribute of that object.

		The given attribute will be set to False once when this object is created, 
		and will be set to True whenever changes have been detected.
		(It is your object's responsibility to handle it in a thread-safe manner.)
		
		Passing a dictionary of files {partial-filepath:modification-time} 
		is optional. The dictionary (or hash table) will serve as a base 
		point for determine what has changed. If none is supplied, one 
		will be built from the current state."""
		
		
		self.__root = 		""	# __root directory in absolute path
		self.__observers = {}
		self.__running = 		1 	# Apparently True has speed issues, it takes LONGER to evaluate!
								# Set to 0 to stop
	
		self.__sleeptime = 0.5
		
	
		self.__lastRunItems = 	folder("")	# What were things like on last cycle? Holds a root folder object
		self.__thisRunItems = 	folder("")	# And this time? Holds a root folder object
		self.__stateLock = 		thread.allocate_lock()		
		self.__changeLog = 		([],[],[])
		self.__changeLock = 	thread.allocate_lock()
		###------------
		
		
		
			
		self.__root = root		
		self.__lastRunItems = items
		
		self.__setAlert(False)
		
		Thread.__init__(self)
		Thread.setDaemon(self,True)
	def getRoot(self):
		return self.__root
	def setTarget(self,observer,funcname):
		# Should be thread-safe, a single construction
		self.__observers[observer] = funcname
	def start(self):
		self.__running = 1
		Thread.start(self)
		self.id = id(self)		
	def stopThread(self):
		""" Signal the monitor to stop."""
		self.__running = 0		
	def getState(self):
		""" Get a copy of the most recently completed file tree."""
		self.__stateLock.acquire()
		ret =  self.__lastRunItems # TODO Make this a real copy for safety?
		self.__stateLock.release()
		return(ret)
	def getEvents(self, clear):
		""" Returns a copy of the current events as tuples.. It takes one boolean
		argument--whether to clear the log at the same time. If you
		cannot afford to miss events, then use this instead of consecutive
		getEvents(0) and clearEvents() calls.
		
		Returns three lists in a tuple (added,deleted,modified)
		Each item in "added" is a File. Try getPathbits()
		Each item in "deleted" is the same.
		Each item in "modified" is a two-part tuple, where the first
			part stores a file, and the second is modificiation time
		
		"""
		
		
		ret = ""
		self.__changeLock.acquire()
		ret = self.__changeLog
		if clear:
			self.__changeLog = ([],[],[])			
		self.__changeLock.release()

		return ret;
					
	def clearEvents(self):
		""" Clears the event log list. """
		self.__changeLock.acquire()		
		self.__changeLog = ([],[],[])	
		self.__changeLock.release()
			
	
	def __setAlert(self,haschange):
		""" Notifies the observer that something has changed """
		for observer in self.__observers:
			attr = self.__observers[observer]
			setattr(observer,attr,bool(haschange))
			
	def __buildTree(self):
		base = folder('')
		for path, dirs, files in os.walk(self.__root, True):	
			dstub = path.split(self.__root)[1] + os.sep
			dstub = dstub.strip(os.sep)
			bits = dstub.split(os.sep)
			owner = base.getDescendantFolder(bits)
			if(owner == None): continue; # Next iter. a ".svn" folder was causing this...?
			for name in files:
				if(name[0] == "."):
					continue # Skip dot-files
				try:
					mtime = int(os.stat(path+os.sep+name).st_mtime)
					owner.add(file(name,mtime))
				except OSError, e:
					# File removed suddenly? Huh. 
					pass
			for name in dirs:
				if(name[0] == "."):
					continue # Skip dot-files
				owner.add(folder(name))
			
		return base			
	def compareTrees(self,older,newer, added, deleted, modified):		
		a = older.getContents()
		b = newer.getContents()
		assert(type(a) == set)
		assert(type(b) == set)
		newerset = (b-a)
		missingset = (a-b)
		next = a.intersection(b)
	
		for (isFolder,name) in newerset:
			if(isFolder):
				added.extend(newer.get(name).getAllFiles())
			else:
				added.append(newer.get(name))
		for (isFolder,name) in missingset:
			if(isFolder):
				deleted.extend(older.get(name).getAllFiles())
			else:
				deleted.append(older.get(name))
		for (isFolder, name) in next:
			if(isFolder):
				self.compareTrees(older.get(name),newer.get(name),added,deleted,modified)
			else:
				# Compare modtimes			
				m1 = older.get(name)
				m2 = newer.get(name)
				if(m1.modtime < m2.modtime):
					modified.append((m2,m2.modtime))			
	
	def __coreProcess(self):
		# Construct our new file tree
		self.__thisRunItems = self.__buildTree()
		
		# Compare our two trees for changes
		self.__changeLock.acquire()		
		
		a,d,m = self.__changeLog
		sizes = (len(a),len(d),len(m))			
		self.compareTrees(self.__lastRunItems,self.__thisRunItems,a,d,m)
		self.__changeLog = (a,d,m)
		self.__changeLock.release()
		if not (len(a),len(d),len(m)) == sizes:
			self.__setAlert(True)
		
		
		#if(len(a)> 0):
		#	print "Added".center(80,"=")
		#	print [i.getPathbits() for i in a]
		#if(len(d)> 0):
		#	print "Deleted".center(80,"=")
		#	print [i.getPathbits() for i in d]
		#if(len(m)> 0):
		#	print "Modified".center(80,"=")
		#	print [(i.getPathbits(),m) for (i,m) in m]
		

		
		# By now, we should have done a full round of checks, 
		# stored the results, and notified our observer of any
		# changes. Let's prepare for another go!
					
		
		self.__stateLock.acquire()
		self.__lastRunItems = self.__thisRunItems
		self.__stateLock.release()
		
	def run(self):
		while self.__running:
			self.__coreProcess()								
			time.sleep(self.__sleeptime)
			
